package com.ys.service.wallet.base;

import cn.hutool.core.codec.Base64Encoder;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSONObject;
import com.googlecode.jsonrpc4j.JsonRpcHttpClient;
import com.ys.entity.WalletAddress;
import com.ys.entity.WalletWithdraw;
import com.ys.entity.block.Block;
import com.ys.entity.block.Gather;
import com.ys.entity.block.Transaction;
import com.ys.entity.vo.Withdraw;
import com.ys.enu.CurrencyEnum;
import com.ys.enu.ResultCodeEnum;
import com.ys.exception.base.BizException;
import com.ys.service.WalletAddressServiceImap;
import com.ys.service.WalletWithdrawServiceImap;
import com.ys.util.EthUtil;
import com.ys.util.HexUtil;
import com.ys.web.base.ResultBody;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

/**
 * 钱包抽象类
 */
@Slf4j
@Data
public abstract class AbstractWalletService implements  IWalletService {

    @Autowired
    private WalletAddressServiceImap walletAddressServiceImap;
    @Autowired
    private WalletWithdrawServiceImap walletWithdrawServiceImap;
    /**
     * 客户端缓存
     */
    private ConcurrentMap<String, JsonRpcHttpClient> cachedClient = new ConcurrentHashMap<>();

    public ResultBody createAddress() {
        try {
            String password=initPassword();
            Object param = new Object[] {password};
            Object obj=createRPCClient().invoke(createAddressMethod(), param, String.class);

             String salt= RandomUtil.randomString(32);//随机6位数字
             String encryptPass=SecureUtil.des(salt.getBytes()).encryptBase64(password);
             log.info("解密后的字符串："+SecureUtil.des(salt.getBytes()).decryptStrFromBase64(encryptPass)+":salt="+salt);
             WalletAddress walletAddress=new WalletAddress();
             walletAddress.setAdress(obj.toString());
             walletAddress.setCurrency(getCurrency());
             walletAddress.setCurrencyChainType(currencyEnum().getCurrencyChainType());
             walletAddress.setPassword(encryptPass);
             walletAddress.setSalt(salt);

             getWalletAddressServiceImap().insert(walletAddress);

             return  new ResultBody(ResultCodeEnum.success,obj);
        } catch ( Throwable e) {
            log.info("创建钱包失败",e);
        }
        return new ResultBody(ResultCodeEnum.fail,"创建钱包失败");
    }



    public String createAddressMethod(){
        return "personal_newAccount";
    }


    public ResultBody validateAddress(String address) {
        try {
            Object param = new Object[] {address};
            return  new ResultBody(ResultCodeEnum.success,createRPCNOAuth().invoke(validateAddressMethod(), param, String.class));
        } catch ( Throwable e) {
            log.info("验证钱包地址失败",e);

        }
        return new ResultBody(ResultCodeEnum.success);

    }
    public String validateAddressMethod(){
        return "validateaddress";
    }


    /**
     * 根据地址查询钱包余额
     * @param address
     * @return
     */
    public ResultBody balanceByAddress(String address) {
        try {
            Object param = new Object[] {address,"latest"};
            Object obj=createRPCClient().invoke(balanceByAddressMethod(), param, String.class);
            return  new ResultBody(ResultCodeEnum.success,obj);
        } catch ( Throwable e) {
            log.info("余额查询失败",e);
        }
        return new ResultBody(ResultCodeEnum.fail,"查询余额失败");
    }
    public String balanceByAddressMethod() {

        return "eth_getBalance";
    }



    /**
     * 提币插入记录
     * @param withdraw
     * @return
     */
    public ResultBody withDraw(Withdraw withdraw) {
        log.info("提币入参：withDraw="+ JSONUtil.toJsonStr(withdraw));
        WalletWithdraw wwd=new WalletWithdraw();
        if(withdraw.getFee()!=null)
            wwd.setWithdrawFee(new BigDecimal(withdraw.getFee()));
        /** 交易hash**/
        try {
            String gasPriceStr = gasPrice();
            if(StrUtil.isBlank(gasPriceStr))
                throw new BizException("获取gas失败", ResultCodeEnum.fail);
            double gasPrice = EthUtil.hexToDouble(gasPriceStr);
            if(withdraw.getFee()!=null && withdraw.getFee()<gasPrice){
                log.warn("手续费太低 fee-gasPrice="+(withdraw.getFee()-gasPrice));
                throw new BizException("手续费太低 fee<gasPriceStr", ResultCodeEnum.fail);
            }else{
                log.info("用户未输入手续费，手续费自动设置");
                withdraw.setFee(gasPrice);
            }
            insertWithdraw(wwd,withdraw,gasPrice);
            log.info("提币记录插入成功：WalletWithdraw="+JSONUtil.toJsonStr(wwd));
        } catch (Throwable e) {
            log.error(" sendToAddress ", e);
            //TODO 应该更新提笔表
            return new ResultBody(ResultCodeEnum.fail,"提币失败");

        }
        return new ResultBody(ResultCodeEnum.success,wwd.getId());
    }

    /**
     * 发送提币交易
     * @param walletWithdraw
     * @return
     */
    public String sendWithDraw(WalletWithdraw walletWithdraw) {
        log.info("提币入参：withDraw="+ JSONUtil.toJsonStr(walletWithdraw));
        /** 交易hash**/
        String transactionID;
        try {
            /** 解锁账号 **/
            unlock(getWithDrawAddress(), withDrawAddressPwd());
            JSONObject sendInfo = new JSONObject();
            sendInfo(sendInfo,walletWithdraw);
            log.info("提币交易信息："+JSONObject.toJSONString(sendInfo));
            transactionID = createRPCNOAuth().invoke("eth_sendTransaction", new Object[] {sendInfo}, String.class);
            log.info("提币成功：transactionID="+transactionID+",sysNum="+walletWithdraw.getSysNum());
            return transactionID;
        } catch (Throwable e) {
            log.error("发送提币交易失败，walletWithdraw="+JSONUtil.toJsonStr(walletWithdraw), e);
        }finally {
            lock(getWithDrawAddress());
        }
        return null;

    }

    /**
     * 归集交易
     * @param gather
     */
    public String sendGatherTransation(Gather gather){
        log.info("归集信息：gather:"+ JSONUtil.toJsonStr(gather));
        try {
            /** 或许要解锁账号 **/
            unlock(gather.getFrom(), gather.getPassword());
            JSONObject sendInfo = new JSONObject();
            sendInfo.put("from", gather.getFrom());
            sendInfo.put("to", getDepositAddress());
            BigDecimal gas=new BigDecimal(fee());

            String  gasPrice=gasPrice();
            BigDecimal  totalGas=EthUtil.double18(gas.doubleValue()).multiply(new BigDecimal(HexUtil.hexToBigInteger(gasPrice())));
            BigInteger  value=new BigDecimal(gather.getAmount()).subtract(totalGas).toBigInteger();

            //TODO
            sendInfo.put("value", HexUtil.bigintegerToHex(value));
            sendInfo.put("gas",EthUtil.doubleToHex(gas.doubleValue()));

            sendInfo.put("gasPrice", gasPrice);
            log.info("归集交易信息："+JSONObject.toJSONString(sendInfo));
            String  transactionID = createRPCNOAuth().invoke("eth_sendTransaction", new Object[] {sendInfo}, String.class);
            log.info("归集成功：transactionID="+transactionID+",gather="+JSONUtil.toJsonStr(gather));
            return transactionID;
        }catch (Throwable e){
            log.warn("归集失败",e);
            log.warn("归集参数：gather="+gather);
        }finally {
            lock(gather.getFrom());
        }
        return null;
    }

    /**
     * 封装交易信息
      * @param sendInfo
     * @param walletWithdraw
     */
    public void sendInfo(JSONObject sendInfo,WalletWithdraw walletWithdraw){
        sendInfo.put("from", walletWithdraw.getFromAddress());
        sendInfo.put("to", walletWithdraw.getToAddress());
        //TODO
        sendInfo.put("value", EthUtil.doubleToHex(walletWithdraw.getWithdrawAmount().doubleValue()));
        sendInfo.put("gas", EthUtil.doubleToHex(walletWithdraw.getWithdrawGas().doubleValue()));

        sendInfo.put("gasPrice", EthUtil.doubleToHex(walletWithdraw.getWithDrawGasPrice().doubleValue()));
    }


    /**
     * 插入提币表
     * @param wwd
     * @param withdraw
     * @param gasPrice
     */
    public void insertWithdraw(WalletWithdraw wwd,Withdraw withdraw,Double gasPrice){
        wwd.setCurrency(currencyEnum().getCurrency());
        wwd.setCurrencyChainType(currencyEnum().getCurrencyChainType());
        wwd.setFromAddress(getWithDrawAddress());
        wwd.setSysNum(withdraw.getSysNum());
        wwd.setToAddress(withdraw.getAddress());
        wwd.setWithdrawAmount(new BigDecimal(withdraw.getAmount()));

        if(withdraw.getFee() >= maxPrice()){
            log.info("手续费高于最大手续费：fee="+withdraw.getFee(),"maxPrice="+maxPrice());
            wwd.setWithdrawGas(new BigDecimal(maxPrice()));
        }else{
            wwd.setWithdrawGas(new BigDecimal(fee()));
        }
        wwd.setWithdrawFee(new BigDecimal(withdraw.getFee()));
        //TODO 需要优化
        wwd.setWithDrawGasPrice(new BigDecimal(gasPrice));
        wwd.setWithdrawInt(-1);
        //插入提币表
        walletWithdrawServiceImap.insert(wwd);
    }

    /**
     * 以太坊解锁账号　交易之前
     * @param address
     * @param password
     * @throws Throwable
     */
    public void unlock(String address,String password) throws Throwable {

        /** 解锁账号 **/
        this.createRPCClient().invoke("personal_unlockAccount",
                new Object[] {address,password}, String.class);

    }
    public void lock(String address) {

        /** 解锁账号 **/
        try {
            this.createRPCClient().invoke("personal_lockAccount",
                    new Object[] {address}, String.class);
        } catch (Throwable throwable) {
            throwable.printStackTrace();
            log.warn("锁定账号失败：address=");
        }

    }

    public String withDrawAddressPwd(){
        return null;
    }
    public abstract String getWithDrawAddress();
    public Double maxPrice(){
        return 0d;
    }
    public double fee(){
        return 0;
    }

    /**
     * 手续费 gas
     * @return
     */
    public String gasPrice(){
        try {
            Object param = new Object[] {};
            return createRPCClient().invoke(gasPriceMethod(), param, String.class);
        } catch ( Throwable e) {
            log.info("获取gasprice失败",e);
        }
        return null;
    }
    public String gasPriceMethod(){

        return "eth_gasPrice";
    }


    @Override
    public Block queryBlockByNum(Integer num , Block b) {
        try {
            Object param = new Object[] {"0x"+num.toHexString(num),true};
          /*  Object ob=createRPCNOAuth().invoke(queryBlockByNumMethod(), param, HashMap.class);
            log.info(JSONUtil.toJsonStr(ob));*/
            return  createRPCNOAuth().invoke(queryBlockByNumMethod(), param, b.getClass());
        } catch ( Throwable e) {
            log.info("查询区块信息失败",e);

        }
        return null;
    }

    public String queryBlockByNumMethod(){

        return "eth_getBlockByNumber";
    }


    @Override
    public String mostRecentBlockNumber() {
        try {
            Object param = new Object[] {};
            return createRPCClient().invoke(mostRecentBlockNumberMethod(), param, String.class);
        } catch ( Throwable e) {
            log.info("获取区块号失败",e);
        }
        return null;
    }


    public String mostRecentBlockNumberMethod(){
        return "eth_blockNumber";
    }


    @Override
    public Transaction queryTransactionByHash(String txHash, Transaction t) {

        try {
            Object param = new Object[] {txHash};
            return createRPCClient().invoke(queryTransactionByHashMethod(), param, t.getClass());
        } catch ( Throwable e) {
            log.info("根据交易hash获取交易信息失败",e);
        }
        return null;
    }

    public String queryTransactionByHashMethod(){

        return "eth_getTransactionByHash";

    }


    @Override
    public BigInteger queryBalanceByAddress(String address) {
        try {
            Object param = new Object[] {address,"latest"};
            log.info(createRPCClient().invoke(queryBalanceByAddressMethod(), param, String.class));
            return formatBalance(createRPCClient().invoke(queryBalanceByAddressMethod(), param, String.class));
        } catch ( Throwable e) {
            log.info("根据地址查询余额失败",e);
        }
        return null;

    }

    public String queryBalanceByAddressMethod(){
        return "eth_getBalance";
    }

    /**
     * 格式化金额
     * @param balance
     * @return
     */
    public BigInteger formatBalance(String balance){
        if(StrUtil.isBlank(balance))
            return null;
        return HexUtil.hexToBigInteger(balance);
    }


    @Override
    public String queryBlockByHash(String hash) {
        return null;
    }

    protected JsonRpcHttpClient createRPCClient() throws MalformedURLException {
        return null == auth() ? createRPCNOAuth() : createRPCDefault();
    }

    public JsonRpcHttpClient createRPCDefault() throws MalformedURLException {
        final JsonRpcHttpClient client;
        if (cachedClient.containsKey(clientCacheKey()))
            return  cachedClient.get(clientCacheKey());
        Map<String, String> authorization = new ConcurrentHashMap<>();
        authorization.put(
                "Authorization",
                "Basic " + Base64Encoder.encode(auth()));
        client = new JsonRpcHttpClient(new URL(server()), authorization);
        client.setConnectionTimeoutMillis(120 * 1000);
        client.setReadTimeoutMillis(120 * 1000);
        cachedClient.put(clientCacheKey(),client );

        return client;
    }

    public JsonRpcHttpClient createRPCNOAuth() throws MalformedURLException {
        final JsonRpcHttpClient client;
        if (cachedClient.containsKey(clientCacheKey()))
            return  cachedClient.get(clientCacheKey());

        Map<String, String> authorization = new ConcurrentHashMap<>();
        authorization.put("Content-type", "application/json");

        client = new JsonRpcHttpClient(new URL(server()),authorization);
        client.setConnectionTimeoutMillis(120 * 1000);
        client.setReadTimeoutMillis(120 * 1000);
        cachedClient.put( clientCacheKey(),client );

        return client;
    }

    public JsonRpcHttpClient createRPCDefaultJson() throws MalformedURLException {
        final JsonRpcHttpClient client;
        if (cachedClient.containsKey(clientCacheKey()))
            return  cachedClient.get(clientCacheKey());

        Map<String, String> authorization = new ConcurrentHashMap<>();
        authorization.put("Content-type", "application/json");
        authorization.put(
                "Authorization",
                "Basic " + Base64Encoder.encode(auth()));
        client = new JsonRpcHttpClient(new URL(server()), authorization);
        client.setConnectionTimeoutMillis(120 * 1000);
        client.setReadTimeoutMillis(120 * 1000);
        cachedClient.put( clientCacheKey(),client );

        return client;
    }



    public JsonRpcHttpClient createACTRPCClient() throws MalformedURLException {
        final JsonRpcHttpClient client;
        if (cachedClient.containsKey(clientCacheKey()))
            return cachedClient.get(clientCacheKey());

        Map<String, String> authorization = new ConcurrentHashMap<>();
        authorization.put("Content-type", "application/json");

        // 6位随机
        String random = (int) ((Math.random() * 9 + 1) * 100000) + "";
        authorization.put(
                "Authorization", random + Base64Encoder.encode(auth()));
        client = new JsonRpcHttpClient(new URL(server()), authorization);
        client.setConnectionTimeoutMillis(120 * 1000);
        client.setReadTimeoutMillis(120 * 1000);
        cachedClient.put( clientCacheKey(),client );

        return client;
    }


    public abstract  String auth();
    public abstract  String server();
    @Override
    public abstract CurrencyEnum currencyEnum();
    /**
     * 资产类型
     * @return
     */
    public abstract Integer getCurrency();
    /**
     * 默认新账号的密码
     * @return
     */
    public String  initPassword(){
        return "123456";
    }

    /**
     * 缓存的jsonrpc 客户端
     * @return
     */
    private String clientCacheKey(){
        return server()+name();
    }
    /**
     * 资产名字
     * @return
     */
    @Override
    public String name() {
        return null;
    }


    public abstract  String getDepositAddress();


}
